package com.eugenes.collections;

import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;

public final class MyHashMap<K, V> implements MyMap<K, V> {

    private final static int DEFAULT_CAPACITY = 1 << 4;
    private final static float DEFAULT_LOAD_FACTOR = 0.75f;

    private int mod = Integer.MIN_VALUE;
    private int capacity = 1;
    private float loadFactor;
    private int size = 0;

    private int nextResize;
    private SimpleEntry[] table = {};

    public MyHashMap(int initialCapacity, float loadFactor) {
        String errMessage = null;
        if (initialCapacity <= 0) {
            errMessage = "Wrong initial capacity: " + initialCapacity;
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor) || Float.isInfinite(loadFactor)) {
            errMessage = "Wrong loadFactor: " + loadFactor;
        }
        if (errMessage != null) {
            throw new IllegalArgumentException(errMessage);
        }

        this.loadFactor = loadFactor;
        ensureCapacity(initialCapacity);
    }

    public MyHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public MyHashMap() {
        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    private void linkEntry(SimpleEntry<K, V> e, int bucketIndex) {
        e.next = table[bucketIndex];
        table[bucketIndex] = e;
    }

    private void fillTable(SimpleEntry[] oldTable) {
        for (SimpleEntry<K, V> e : oldTable) {
            SimpleEntry<K, V> current = e;
            while (current != null) {
                int index = getBucketIndex(current.hash);
                SimpleEntry<K, V> nxt = current.next;
                linkEntry(current, index);
                current = nxt;
            }
        }
    }

    private void ensureCapacity(int minCapacity) {
        long newCapacity = capacity;
        while (minCapacity > newCapacity) {
            newCapacity <<= 1;
            if (newCapacity > Integer.MAX_VALUE) {
                newCapacity >>= 1;
                break;
            }
        }

        if (newCapacity != capacity) {
            capacity = (int)newCapacity;
            nextResize = (int) (capacity * loadFactor);
            SimpleEntry[] oldTable = table;
            table = new SimpleEntry[capacity];
            fillTable(oldTable);
        }
    }

    private void checkGrow() {
        if (size >= nextResize) {
            ensureCapacity(2 * capacity);
        }
    }

    private void incSize() {
        ++mod;
        ++size;
        checkGrow();
    }
    private void decSize() {
        ++mod;
        --size;
    }

    private static int getHashCode(Object obj) {
        return obj == null ? 0 : obj.hashCode();
    }

    private int getBucketIndex(int hashCode) {
        return hashCode % table.length;
    }

    private SimpleEntry<K, V> getEntry(Object obj) {
        int hashCode = getHashCode(obj);
        int bucketIndex = getBucketIndex(hashCode);
        for (SimpleEntry<K, V> current = table[bucketIndex];
                current != null;
                current = current.next) {
            if (current.keyIsEqual(obj)) {
                return current;
            }
        }

        return null;
    }

    @Override
    public V put(K key, V value) {
        SimpleEntry<K, V> entry = getEntry(key);
        if (entry != null) {
            V result = entry.getValue();
            entry.setValue(value);
            return result;
        }

        int hashCode = getHashCode(key);
        incSize();
        int bucketIndex = getBucketIndex(hashCode);
        SimpleEntry<K, V> newEntry = new SimpleEntry<>(key, value, hashCode);
        linkEntry(newEntry, bucketIndex);

        return null;
    }

    private static void clearEntry(SimpleEntry<?, ?> entry) {
        entry.key = null;
        entry.value = null;
        entry.next = null;
    }

    @Override
    public V remove(Object obj) {
        int hashCode = getHashCode(obj);
        int bucketIndex = getBucketIndex(hashCode);

        SimpleEntry<K, V> prevBucket = null;
        for (SimpleEntry<K, V> current = table[bucketIndex];
                current != null;
                current = current.next) {
            if (current.keyIsEqual(obj)) {
                if (prevBucket != null) {
                    prevBucket.next = current.next;
                } else {
                    table[bucketIndex] = current.next;
                }
                decSize();
                V result = current.getValue();
                clearEntry(current);
                return result;
            }
            prevBucket = current;
        }

        return null;
    }

    @Override
    public void clear() {
        capacity = 1;
        size = 0;
        for (SimpleEntry<K, V> e : table) {
            SimpleEntry<K, V> current = e;
            while (current != null) {
                SimpleEntry<K, V> nxt = current.next;
                clearEntry(current);
                current = nxt;
            }
        }
        ensureCapacity(DEFAULT_CAPACITY);
        ++mod;
    }

    @Override
    public Iterator<Entry<K, V>> entryIterator() {
        return new HashMapIterator();
    }

    @Override
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    @Override
    public boolean containsValue(Object value) {
        for (SimpleEntry<K, V> e : table) {
            SimpleEntry<K, V> current = e;
            while (current != null) {
                if (current.valueIsEqual(value)) {
                    return true;
                }
                current = current.next;
            }
        }
        return false;
    }

    @Override
    public V get(Object key) {
        Entry<K, V> entry = getEntry(key);
        return entry == null ? null : entry.getValue();
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return size;
    }

    private class HashMapIterator implements Iterator<Entry<K, V>> {

        int lastMod = mod;
        int currentBucket = -1;
        int returnCount = 0;
        boolean canRemove;

        SimpleEntry<K, V> entry;
        SimpleEntry<K, V> prevEntry;

        private void checkMod() {
            if (lastMod != mod) {
                throw new ConcurrentModificationException("List was modified since interator creation");
            }
        }

        private void moveToNextEntry() {
            if (entry != null) {
                prevEntry = entry;
                entry = entry.next;
            }

            if (entry == null) {
                while (++currentBucket < capacity
                        && ((entry = table[currentBucket])== null)) {}
                prevEntry = null;
            }
        }

        @Override
        public boolean hasNext() {
            checkMod();
            return returnCount != size;
        }

        @Override
        public Entry next() {
            checkMod();
            moveToNextEntry();
            if (entry == null) {
                throw new NoSuchElementException("Iterator is out of bounds.");
            }
            canRemove = true;
            ++returnCount;
            return entry;
        }

        @Override
        public void remove() {
            checkMod();
            if (!canRemove) {
                throw new IllegalStateException("Call next method before removing.");
            }

            canRemove = false;
            --returnCount;
            --size;

            if (prevEntry == null) {
                table[currentBucket] = entry.next;
                clearEntry(entry);
                entry = table[currentBucket];
            } else {
                prevEntry.next = entry.next;
                clearEntry(entry);
                entry = prevEntry.next;
            }
        }
    }

    static class SimpleEntry<K, V> implements Entry<K, V> {
        K key;
        V value;
        int hash;

        SimpleEntry next;

        SimpleEntry(K key, V value, int hash) {
            this.key = key;
            this.value = value;
            this.hash = hash;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V value) {
            V result = this.value;
            this.value = value;
            return result;
        }

        boolean keyIsEqual(Object obj) {
            return obj == null ? getKey() == null : obj.equals(getKey());
        }

        boolean valueIsEqual(Object obj) {
            return obj == null ? getValue() == null : obj.equals(getValue());
        }
    }
}
